package com.monochromebytes.mononet.virtual;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;

public class Connection {

    private final ConnectionSocket              socket;
    private final VirtualAddress                address;
    private final QueuedInbox                   inbox;
    private final QueuedOutbox                  outbox;
    private final ArrayList<PacketListener>     listeners;
    private final ArrayList<OutgoingDataPacket> sentPackets;
    private final Acknowledgment                acknowledgment;
    private final SequenceNumber                localSequenceNumber;
    private final SequenceNumber                remoteSequenceNumber;

    protected Connection(ConnectionSocket socket, VirtualAddress address) {
        this.socket = socket;
        this.address = address;
        this.inbox = new QueuedInbox();
        this.outbox = socket.getOutbox();
        this.localSequenceNumber = new SequenceNumber(1);
        this.remoteSequenceNumber = new SequenceNumber(1);
        this.acknowledgment = new Acknowledgment();
        this.acknowledgment.setBase(this.remoteSequenceNumber);

        this.sentPackets = new ArrayList<OutgoingDataPacket>();
        this.listeners = new ArrayList<PacketListener>();
    }

    public void removeFromSocket() {
        this.socket.removeConnection(this);
    }

    public synchronized void handleNextPacket() {
        IncomingDataPacket packet = this.inbox.getNextPacket();

        // Update remote sequence number to packet sequence number if more
        // recent.
        SequenceNumber packetSequenceNumber = packet.getSequenceNumber();
        if (packetSequenceNumber.isMoreRecentThan(this.remoteSequenceNumber)) {
            this.remoteSequenceNumber.set(packetSequenceNumber);
            this.acknowledgment.setBase(this.remoteSequenceNumber);
        }
        this.acknowledgment.addAcknowledge(packetSequenceNumber);

        // Check if packet is acknowledged.
        for (Iterator<OutgoingDataPacket> iterator = this.sentPackets.iterator(); iterator
                .hasNext();) {
            OutgoingDataPacket sentPacket = iterator.next();

            if (packet.isAcknowledged(sentPacket)) {
                // Notify listeners.
                synchronized (this.listeners) {
                    for (PacketListener listener : this.listeners) {
                        listener.acknowledgedPacket(sentPacket, this);
                    }
                }

                // Remove Packet from list.
                iterator.remove();
            }
        }

        // Notify listeners.
        synchronized (this.listeners) {
            for (PacketListener listener : this.listeners) {
                listener.receivePacket(packet, this);
            }
        }
    }

    public synchronized void checkForLostPackets() {
        for (Iterator<OutgoingDataPacket> iterator = this.sentPackets.iterator(); iterator
                .hasNext();) {
            OutgoingDataPacket sentPacket = iterator.next();

            if (sentPacket.isLost()) {
                // Notify listeners.

                synchronized (this.listeners) {
                    for (PacketListener listener : this.listeners) {
                        listener.lostPacket(sentPacket, this);
                    }
                }
                // Remove Packet from list.
                iterator.remove();
            }
        }
    }

    public boolean hasIncomingPackets() {
        return this.inbox.size() > 0;
    }

    public void clearInbox() {
        this.inbox.clear();
    }

    public synchronized OutgoingDataPacket sendPacket(ByteBuffer data, int timeToLive) {
        // Determine packet length. TODO
        int limit = data.limit();
        if (data.remaining() > DataPacket.MAX_LENGTH) {
            data.limit(data.position() + DataPacket.MAX_LENGTH);
        }

        // Increase local sequence number.
        this.localSequenceNumber.increase(1);

        // Create packet.
        OutgoingDataPacket packet = new OutgoingDataPacket(this.address, this.localSequenceNumber,
                this.acknowledgment, data.remaining(), timeToLive);
        packet.putData(data);
        data.limit(limit);

        // Send packet.
        this.outbox.sendPacket(packet);
        this.sentPackets.add(packet);

        return packet;
    }

    public void addPacketListener(PacketListener listener) {
        synchronized (this.listeners) {
            this.listeners.add(listener);
        }
    }

    public void removePacketLister(PacketListener listener) {
        synchronized (this.listeners) {
            this.listeners.remove(listener);
        }
    }

    public VirtualAddress getAddress() {
        return this.address;
    }

    public QueuedInbox getInbox() {
        return this.inbox;
    }

    public QueuedOutbox getOutbox() {
        return this.outbox;
    }

    public ConnectionSocket getSocket() {
        return this.socket;
    }
}
